name = "Double Elimination"
description = "Winners bracket and Losers bracket. Lose twice and you're out."
author = "uewepuep"

function generate(round, pilots, channels, options)
    local max         = options.max_pilots_per_race
    local total       = #pilots
    if not is_first_round() then
        pilots = pilots_with_results(pilots)
    end

    local winners = {}
    local losers  = {}

    for _, pilot in ipairs(pilots) do
        local bracket = get_bracket(pilot.id)

        if bracket == "None" then
            table.insert(winners, pilot.id)

        elseif bracket == "Winners" then
            if top_half(pilot.id) then
                table.insert(winners, pilot.id)
            else
                table.insert(losers, pilot.id)
            end

        elseif bracket == "Losers" then
            if top_half(pilot.id) then
                table.insert(losers, pilot.id)
            end
        end
    end

    -- When fewer active pilots remain than fit in one race, collapse into a single final
    if not is_first_round() then
        local all_active = {}
        for _, id in ipairs(winners) do table.insert(all_active, id) end
        for _, id in ipairs(losers) do table.insert(all_active, id) end

        -- Collapse into a single grand final only when:
        --   1. every race in the current round has finished (so we have complete information), AND
        --   2. the remaining pilots all fit in one race, AND
        --   3. there are no Losers bracket pilots left (pure Winners final) OR it is a true
        --      head-to-head grand final (at most one pilot per bracket).
        -- Without condition 1 the collapse fires prematurely after the very first race ends,
        -- before the other pilots in the round have even raced.
        local is_grand_final = #winners <= 1 and #losers <= 1
        if all_results_in() and #all_active > 0 and #all_active <= max and (#losers == 0 or is_grand_final) then
            return {
                {
                    bracket = "Winners",
                    pilots  = minimise_channel_change(all_active)
                }
            }
        end
    end

    local winner_race_count, loser_race_count
    if is_first_round() then
        winner_race_count = math.ceil(total / max)
        loser_race_count  = 0
    else
        winner_race_count = math.ceil(#winners / max)
        loser_race_count  = math.ceil(#losers / max)
    end

    local races = {}

    local function distribute(pilot_ids, n_races, bracket)
        local buckets = {}
        for i = 1, n_races do buckets[i] = {} end

        local sorted = sort_by(pilot_ids, function(id)
            return get_best_consecutive_laps(id, 1)
        end)

        for i, id in ipairs(sorted) do
            table.insert(buckets[((i - 1) % n_races) + 1], id)
        end

        for _, bucket in ipairs(buckets) do
            table.insert(races, {
                bracket = bracket,
                pilots  = minimise_channel_change(bucket)
            })
        end
    end

    if winner_race_count > 0 then
        distribute(winners, winner_race_count, "Winners")
    end
    if loser_race_count > 0 then
        distribute(losers, loser_race_count, "Losers")
    end

    return races
end

function standings(pilots, options)
    local total = #pilots
    local active_winners = {}
    local active_losers  = {}
    local eliminated     = {}

    local function add_eliminated(p)
        local results = get_results(p.id)
        if #results > 0 then
            local last_round    = 0
            local last_position = 0
            for _, r in ipairs(results) do
                if r.round > last_round then
                    last_round    = r.round
                    last_position = r.position
                end
            end
            table.insert(eliminated, {
                id            = p.id,
                name          = p.name,
                result_count  = #results,
                last_round    = last_round,
                last_position = last_position
            })
        end
    end

    for _, p in ipairs(pilots) do
        local bracket = get_bracket(p.id)
        if bracket == "Winners" then
            table.insert(active_winners, p)
        elseif bracket == "Losers" then
            if top_half(p.id) then
                table.insert(active_losers, p)
            else
                -- bottom half of a finished Losers race = eliminated
                add_eliminated(p)
            end
        else
            -- Pilot not in current round. Check the previous round to guard against
            -- premature round generation while earlier races are still in progress.
            local prev_bracket = get_bracket(p.id, -2)
            if prev_bracket == "None" then
                -- not in previous round either = eliminated in an earlier round
                add_eliminated(p)
            elseif prev_bracket == "Losers" and not top_half(p.id, -2) then
                -- finished bottom half of previous round's Losers race = second loss = eliminated
                add_eliminated(p)
            end
            -- if the previous round race is still running, top_half returns true and we
            -- leave this pilot out of the eliminated list until the race is complete
        end
    end

    -- Sort ascending: worst first (gets the highest final position number)
    -- Priority: fewer rounds < earlier last round < worse race finish
    -- last_position is subtracted because lower = better finish = should rank higher
    eliminated = sort_by(eliminated, function(p)
        return p.result_count * 10000 + p.last_round * 100 - p.last_position
    end)

    -- Assign positions: i=1 (worst, eliminated first) → total; i=n (best) → total - n + 1
    local n_elim = #eliminated
    for i, p in ipairs(eliminated) do
        p.position = total - i + 1
    end

    local rows = {}

    for _, p in ipairs(active_winners) do
        table.insert(rows, { pilot_id = p.id, name = p.name, values = { "Winners" } })
    end
    for _, p in ipairs(active_losers) do
        table.insert(rows, { pilot_id = p.id, name = p.name, values = { "Losers" } })
    end

    -- Show best-eliminated first (last to be knocked out nearest the top)
    for i = n_elim, 1, -1 do
        local p = eliminated[i]
        table.insert(rows, { pilot_id = p.id, name = p.name, values = { ordinal(p.position) } })
    end

    return { headings = { "Status" }, rows = rows }
end
